/*-------------------------------------------------------------------------
 *
 * gistvacuum.c
 *	  Postgres GiST索引访问方法的清理程序例程。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/access/gist/gistvacuum.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/genam.h"
#include "access/gist_private.h"
#include "access/transam.h"
#include "commands/vacuum.h"
#include "lib/integerset.h"
#include "miscadmin.h"
#include "storage/indexfsm.h"
#include "storage/lmgr.h"
#include "utils/memutils.h"

/* 需要的工作状态，由 gistbulkdelete 提供 */
typedef struct
{
	IndexVacuumInfo *info;
	IndexBulkDeleteResult *stats;
	IndexBulkDeleteCallback callback;
	void	   *callback_state;
	GistNSN		startNSN;

	/*
	 * 这些用于记住所有内部和空的叶子页面。它们
	 * 用于删除所有空页面。
	 */
	IntegerSet *internal_page_set;
	IntegerSet *empty_leaf_set;
	MemoryContext page_set_context;
} GistVacState;

static void fc_gistvacuumscan(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
						   IndexBulkDeleteCallback fc_callback, void *fc_callback_state);
static void fc_gistvacuumpage(GistVacState *fc_vstate, BlockNumber fc_blkno,
						   BlockNumber fc_orig_blkno);
static void fc_gistvacuum_delete_empty_pages(IndexVacuumInfo *fc_info,
										  GistVacState *fc_vstate);
static bool fc_gistdeletepage(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
						   Buffer fc_buffer, OffsetNumber fc_downlink,
						   Buffer fc_leafBuffer);

/*
 * VACUUM bulkdelete 阶段：移除索引条目。
 */
IndexBulkDeleteResult * gistbulkdelete(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
			   IndexBulkDeleteCallback fc_callback, void *fc_callback_state)
{
	/* 如果第一次遍历则分配统计信息，否则重用现有结构 */
	if (fc_stats == NULL)
		fc_stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));

	fc_gistvacuumscan(fc_info, fc_stats, fc_callback, fc_callback_state);

	return fc_stats;
}

/*
 * VACUUM 清理阶段：删除空页面，并更新索引统计信息。
 */
IndexBulkDeleteResult * gistvacuumcleanup(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats)
{
	/* 在ANALYZE ONLY模式下无操作 */
	if (fc_info->analyze_only)
		return fc_stats;

	/*
	 * 如果调用了 gistbulkdelete，我们无需做任何事情，只需返回
	 * 最新的 gistbulkdelete 调用中的统计信息。如果没有被调用，
	 * 我们仍然需要遍历索引，以获取索引统计信息。
	 */
	if (fc_stats == NULL)
	{
		fc_stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
		fc_gistvacuumscan(fc_info, fc_stats, NULL, NULL);
	}

	/*
	 * 我们可能会被并发页面拆分所欺骗，导致某些索引元组被重复计数，
	 * 因此不相信任何超过底层堆计数的总数……如果我们准确知道这一点。
	 * 否则这可能会使问题变得更糟。
	 */
	if (!fc_info->estimated_count)
	{
		if (fc_stats->num_index_tuples > fc_info->num_heap_tuples)
			fc_stats->num_index_tuples = fc_info->num_heap_tuples;
	}

	return fc_stats;
}

/*
 * gistvacuumscan --- 扫描索引以进行 VACUUM 的目的
 *
 * 这个扫描索引以查找根据
 * VACUUM 回调可以删除的叶子元组，并更新统计信息。btbulkdelete 和
 * btvacuumcleanup 都会调用这个（后者仅在没有发生 btbulkdelete 调用时）。
 *
 * 这还记录了任何空的叶子页面，以及在循环遍历所有索引页面时所有内部
 * 页面。在扫描完所有页面后，我们会删除空页面，以便可以重复使用。
 * 任何已删除的页面会直接添加到自由空间映射中。（它们本该在最初删除时
 * 添加到那里，但例如在崩溃时 FSM 可能丢失。）
 *
 * 调用者负责最初分配/清零统计结构。
 */
static void fc_gistvacuumscan(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
			   IndexBulkDeleteCallback fc_callback, void *fc_callback_state)
{
	Relation	fc_rel = fc_info->index;
	GistVacState fc_vstate;
	BlockNumber fc_num_pages;
	bool		fc_needLock;
	BlockNumber fc_blkno;
	MemoryContext fc_oldctx;

	/*
	 * 现在重置跟踪有关整个索引的信息的字段。这
	 * 避免在单个 VACUUM 命令需要多次扫描索引的情况下发生重复计数。
	 *
	 * 避免在这里重置 tuples_removed 和 pages_newly_deleted 字段，
	 * 因为它们跟踪有关 VACUUM 命令的信息，因此必须在每次调用
	 * gistvacuumscan() 时保持不变。
	 *
	 * （注意，pages_free 被视为有关整个索引的状态，而不是
	 * 当前的 VACUUM。这是合适的，因为 RecordFreeIndexPage()
	 * 调用是幂等的，并且在某些情况下会对相同的已删除页面重复调用。
	 * 对我们来说，关键是跟踪 VACUUM 命令结束时索引中可回收页面的数量。）
	 */
	fc_stats->num_pages = 0;
	fc_stats->estimated_count = false;
	fc_stats->num_index_tuples = 0;
	fc_stats->pages_deleted = 0;
	fc_stats->pages_free = 0;

	/*
	 * 创建整数集合以记住 page_set_context 中所有内部和空叶子
	 * 页面。在内部，整数集合将记住
	 * 此上下文，以便随后为这些整数集合进行的分配将来自相同的上下文。
	 *
	 * XXX 下面使用的分配大小早于生成上下文的块
	 * 增长代码。这些值可能应进行基准测试并设置为
	 * 更合适的值。
	 */
	fc_vstate.page_set_context = GenerationContextCreate(CurrentMemoryContext,
													  "GiST VACUUM page set context",
													  16 * 1024,
													  16 * 1024,
													  16 * 1024);
	fc_oldctx = MemoryContextSwitchTo(fc_vstate.page_set_context);
	fc_vstate.internal_page_set = intset_create();
	fc_vstate.empty_leaf_set = intset_create();
	MemoryContextSwitchTo(fc_oldctx);

	/* 设置要传递给gistvacuumpage的信息 */
	fc_vstate.info = fc_info;
	fc_vstate.stats = fc_stats;
	fc_vstate.callback = fc_callback;
	fc_vstate.callback_state = fc_callback_state;
	if (RelationNeedsWAL(fc_rel))
		fc_vstate.startNSN = GetInsertRecPtr();
	else
		fc_vstate.startNSN = gistGetFakeLSN(fc_rel);

	/*
	 * 外层循环按物理顺序遍历所有索引页面（我们希望内核能配合提供提前读取以提高速度）。 重要的是我们要访问所有叶页面，包括在我们开始扫描后添加的页面，否则我们可能会无法删除一些可删除的元组。
	 * 因此，我们必须反复检查关系的长度。 我们在执行此操作时必须获取关系扩展锁，以避免竞争条件：
	 * 如果其他人正在扩展关系，在bufmgr/smgr创建新全零页面但尚未被gistNewBuffer()写锁定的窗口内，可能会出现问题。 如果我们在这里成功扫描到这样的页面，我们将错误地假设它可以被回收。 获取锁可以足够同步，防止问题的发生：要么num_pages将不包括新页面，要么gistNewBuffer已在缓冲区上获取写锁并将在我们检查之前完全初始化。（另请参见vacuumlazy.c，它有相同的问题。）此外，如果我们看的时候立即添加了一个页面，我们不必担心；页面分割代码在添加右页面之前已经对左页面获取了写锁，因此我们必须已经处理了任何应当移动到该页面的元组。
	 *
	 * 然而，对于新的或临时关系，我们可以跳过锁定，因为没有其他人可以访问它们。
	 */
	fc_needLock = !RELATION_IS_LOCAL(fc_rel);

	fc_blkno = GIST_ROOT_BLKNO;
	for (;;)
	{
		/* 获取当前关系长度 */
		if (fc_needLock)
			LockRelationForExtension(fc_rel, ExclusiveLock);
		fc_num_pages = RelationGetNumberOfBlocks(fc_rel);
		if (fc_needLock)
			UnlockRelationForExtension(fc_rel, ExclusiveLock);

		/* 如果我们扫描了整个关系，则退出 */
		if (fc_blkno >= fc_num_pages)
			break;
		/* 遍历页面，然后循环回去重新检查长度 */
		for (; fc_blkno < fc_num_pages; fc_blkno++)
			fc_gistvacuumpage(&fc_vstate, fc_blkno, fc_blkno);
	}

	/*
	 * 如果我们发现了任何可回收页面（并在FSM中记录了它们），那么强制更新上层FSM页面，以确保搜索者可以找到它们。 这些页面可能在之前的扫描中也被找到，因此这可能是浪费时间，但就相对于扫描索引而言，成本足够低，应该不会有太大影响，并且确保尽早而不是晚些时候可用空闲页面似乎是值得的。
	 *
	 * 注意，如果不存在可回收页面，我们就不需要清理FSM了。
	 */
	if (fc_stats->pages_free > 0)
		IndexFreeSpaceMapVacuum(fc_rel);

	/* 更新统计信息 */
	fc_stats->num_pages = fc_num_pages;

	/*
	 * 如果我们看到任何空页面，尝试将它们与树解除链接，以便可以重用。
	 */
	fc_gistvacuum_delete_empty_pages(fc_info, &fc_vstate);

	/* 我们不再需要内部和空页面集合 */
	MemoryContextDelete(fc_vstate.page_set_context);
	fc_vstate.page_set_context = NULL;
	fc_vstate.internal_page_set = NULL;
	fc_vstate.empty_leaf_set = NULL;
}

/*
 * gistvacuumpage --- 清理一个页面
 *
 * 这是针对gistbulkdelete()处理单个页面。在某些情况下，我们必须回去重新检查之前扫描的页面；当有必要时，这个例程会递归调用以处理这种情况。
 *
 * blkno是要处理的页面。 orig_blkno是外部gistvacuumscan循环到达的最高块号（与blkno相同，除非我们在递归时重新检查之前的页面）。
 */
static void fc_gistvacuumpage(GistVacState *fc_vstate, BlockNumber fc_blkno, BlockNumber fc_orig_blkno)
{
	IndexVacuumInfo *fc_info = fc_vstate->info;
	IndexBulkDeleteCallback fc_callback = fc_vstate->callback;
	void	   *fc_callback_state = fc_vstate->callback_state;
	Relation	fc_rel = fc_info->index;
	Buffer		fc_buffer;
	Page		fc_page;
	BlockNumber fc_recurse_to;

restart:
	fc_recurse_to = InvalidBlockNumber;

	/* 在不持有任何缓冲区锁的情况下调用vacuum_delay_point */
	vacuum_delay_point();

	fc_buffer = ReadBufferExtended(fc_rel, MAIN_FORKNUM, fc_blkno, RBM_NORMAL,
								fc_info->strategy);

	/*
	 * 我们不会在这里停留太长时间，积极地获取独占锁。
	 */
	LockBuffer(fc_buffer, GIST_EXCLUSIVE);
	fc_page = (Page) BufferGetPage(fc_buffer);

	if (gistPageRecyclable(fc_page))
	{
		/* 可以回收此页面 */
		RecordFreeIndexPage(fc_rel, fc_blkno);
		fc_vstate->stats->pages_deleted++;
		fc_vstate->stats->pages_free++;
	}
	else if (GistPageIsDeleted(fc_page))
	{
		/* 已经删除，但尚不能回收 */
		fc_vstate->stats->pages_deleted++;
	}
	else if (GistPageIsLeaf(fc_page))
	{
		OffsetNumber fc_todelete[MaxOffsetNumber];
		int			fc_ntodelete = 0;
		int			fc_nremain;
		GISTPageOpaque fc_opaque = GistPageGetOpaque(fc_page);
		OffsetNumber fc_maxoff = PageGetMaxOffsetNumber(fc_page);

		/*
		 * 检查我们是否需要递归回到早期页面。我们关心的是自开始
		 * vacuum 扫描以来发生的页面分裂。如果分裂将一些元组移到了
		 * 较低的页面，那么我们可能会错过它们。如果是这样，设置
		 * 以进行尾递归。
		 *
		 * 这类似于我们在搜索期间进行的检查，当跟随一个下行链接
		 * 时，但我们不需要跳转到更高编号的页面，因为我们无论如何
		 * 会稍后处理它们。
		 */
		if ((GistFollowRight(fc_page) ||
			 fc_vstate->startNSN < GistPageGetNSN(fc_page)) &&
			(fc_opaque->rightlink != InvalidBlockNumber) &&
			(fc_opaque->rightlink < fc_orig_blkno))
		{
			fc_recurse_to = fc_opaque->rightlink;
		}

		/*
		 * 扫描所有项目，查看哪些需要根据回调函数删除。
		 */
		if (fc_callback)
		{
			OffsetNumber fc_off;

			for (fc_off = FirstOffsetNumber;
				 fc_off <= fc_maxoff;
				 fc_off = OffsetNumberNext(fc_off))
			{
				ItemId		fc_iid = PageGetItemId(fc_page, fc_off);
				IndexTuple	fc_idxtuple = (IndexTuple) PageGetItem(fc_page, fc_iid);

				if (fc_callback(&(fc_idxtuple->t_tid), fc_callback_state))
					fc_todelete[fc_ntodelete++] = fc_off;
			}
		}

		/*
		 * 应用任何需要的删除。我们每个页面只发布一个 WAL 记录，
		 * 以尽量减少 WAL 流量。
		 */
		if (fc_ntodelete > 0)
		{
			START_CRIT_SECTION();

			MarkBufferDirty(fc_buffer);

			PageIndexMultiDelete(fc_page, fc_todelete, fc_ntodelete);
			GistMarkTuplesDeleted(fc_page);

			if (RelationNeedsWAL(fc_rel))
			{
				XLogRecPtr	fc_recptr;

				fc_recptr = gistXLogUpdate(fc_buffer,
										fc_todelete, fc_ntodelete,
										NULL, 0, InvalidBuffer);
				PageSetLSN(fc_page, fc_recptr);
			}
			else
				PageSetLSN(fc_page, gistGetFakeLSN(fc_rel));

			END_CRIT_SECTION();

			fc_vstate->stats->tuples_removed += fc_ntodelete;
			/* 必须重新计算 maxoff */
			fc_maxoff = PageGetMaxOffsetNumber(fc_page);
		}

		fc_nremain = fc_maxoff - FirstOffsetNumber + 1;
		if (fc_nremain == 0)
		{
			/*
			 * 该页面现在完全为空。记住它的块编号，以便在第二阶段时
			 * 尝试删除该页面。
			 *
			 * 当进行递归时跳过此操作，因为 IntegerSet 要求值以
			 * 升序添加。下一个 VACUUM 将拾取它。
			 */
			if (fc_blkno == fc_orig_blkno)
				intset_add_member(fc_vstate->empty_leaf_set, fc_blkno);
		}
		else
			fc_vstate->stats->num_index_tuples += fc_nremain;
	}
	else
	{
		/*
		 * 在内部页面上，检查 "无效元组"，它们是由 PostgreSQL 9.0
		 * 或更低版本的未完成页面分裂留下的。这些并不是由更新的
		 * PostgreSQL 版本创建的，但不幸的是，GiST 索引中没有
		 * 版本号，因此我们不知道该索引是否仍可能包含无效元组。
		 */
		OffsetNumber fc_maxoff = PageGetMaxOffsetNumber(fc_page);
		OffsetNumber fc_off;

		for (fc_off = FirstOffsetNumber;
			 fc_off <= fc_maxoff;
			 fc_off = OffsetNumberNext(fc_off))
		{
			ItemId		fc_iid = PageGetItemId(fc_page, fc_off);
			IndexTuple	fc_idxtuple = (IndexTuple) PageGetItem(fc_page, fc_iid);

			if (GistTupleIsInvalid(fc_idxtuple))
				ereport(LOG,
						(errmsg("index \"%s\" contains an inner tuple marked as invalid",
								RelationGetRelationName(fc_rel)),
						 errdetail("This is caused by an incomplete page split at crash recovery before upgrading to PostgreSQL 9.1."),
						 errhint("Please REINDEX it.")));
		}

		/*
		 * 记住此页面的块编号，以便稍后在 gistvacuum_delete_empty_pages()
		 * 中重新访问它，当我们搜索空叶页面的父页面时。
		 */
		if (fc_blkno == fc_orig_blkno)
			intset_add_member(fc_vstate->internal_page_set, fc_blkno);
	}

	UnlockReleaseBuffer(fc_buffer);

	/*
	 * 这实际上是尾递归，但如果编译器过于愚蠢而无法将其优化成
	 * 这样，我们在每个递归层级上会消耗不舒服的大量栈空间（由于
	 * 可删除数组）。失败的可能性不大，因为层级数量不太可能大……
	 * 但以防万一，让我们手动优化为循环。
	 */
	if (fc_recurse_to != InvalidBlockNumber)
	{
		fc_blkno = fc_recurse_to;
		goto restart;
	}
}

/*
 * 扫描所有内部页面，并尝试删除其空子页面。
 */
static void fc_gistvacuum_delete_empty_pages(IndexVacuumInfo *fc_info, GistVacState *fc_vstate)
{
	Relation	fc_rel = fc_info->index;
	BlockNumber fc_empty_pages_remaining;
	uint64		fc_blkno;

	/*
	 * 重新扫描所有内部页面以找到那些有空子页面的页面。
	 */
	fc_empty_pages_remaining = intset_num_entries(fc_vstate->empty_leaf_set);
	intset_begin_iterate(fc_vstate->internal_page_set);
	while (fc_empty_pages_remaining > 0 &&
		   intset_iterate_next(fc_vstate->internal_page_set, &fc_blkno))
	{
		Buffer		fc_buffer;
		Page		fc_page;
		OffsetNumber fc_off,
					fc_maxoff;
		OffsetNumber fc_todelete[MaxOffsetNumber];
		BlockNumber fc_leafs_to_delete[MaxOffsetNumber];
		int			fc_ntodelete;
		int			fc_deleted;

		fc_buffer = ReadBufferExtended(fc_rel, MAIN_FORKNUM, (BlockNumber) fc_blkno,
									RBM_NORMAL, fc_info->strategy);

		LockBuffer(fc_buffer, GIST_SHARE);
		fc_page = (Page) BufferGetPage(fc_buffer);

		if (PageIsNew(fc_page) || GistPageIsDeleted(fc_page) || GistPageIsLeaf(fc_page))
		{
			/*
			 * 此页面之前是一个内部页面，但现在变成了其他东西。
			 * 不应该发生……
			 */
			Assert(false);
			UnlockReleaseBuffer(fc_buffer);
			continue;
		}

		/*
		 * 扫描所有下行链接，看看它们中是否有指向空叶页面的。
		 */
		fc_maxoff = PageGetMaxOffsetNumber(fc_page);
		fc_ntodelete = 0;
		for (fc_off = FirstOffsetNumber;
			 fc_off <= fc_maxoff && fc_ntodelete < fc_maxoff - 1;
			 fc_off = OffsetNumberNext(fc_off))
		{
			ItemId		fc_iid = PageGetItemId(fc_page, fc_off);
			IndexTuple	fc_idxtuple = (IndexTuple) PageGetItem(fc_page, fc_iid);
			BlockNumber fc_leafblk;

			fc_leafblk = ItemPointerGetBlockNumber(&(fc_idxtuple->t_tid));
			if (intset_is_member(fc_vstate->empty_leaf_set, fc_leafblk))
			{
				fc_leafs_to_delete[fc_ntodelete] = fc_leafblk;
				fc_todelete[fc_ntodelete++] = fc_off;
			}
		}

		/*
		 * 为了避免死锁，子页必须在父页之前被锁定，因此我们必须释放父页的锁，锁定子页，然后重新获取父页的锁。（而且我们不想在持有锁的情况下进行 I/O。）
		 *
		 * 在我们不持有父页的锁的瞬间，可能会因为并发插入而移动下行链接，因此在我们获取了两个锁之后，必须重新检查它是否仍然指向相同的子页。此外，另一个后端可能已向该页插入了一个元组，因此它不再为空。gistdeletepage()会重新检查所有这些条件。
		 */
		LockBuffer(fc_buffer, GIST_UNLOCK);

		fc_deleted = 0;
		for (int fc_i = 0; fc_i < fc_ntodelete; fc_i++)
		{
			Buffer		fc_leafbuf;

			/*
			 * 不要从父页中移除最后一个下行链接。这会使插入代码感到困惑。
			 */
			if (PageGetMaxOffsetNumber(fc_page) == FirstOffsetNumber)
				break;

			fc_leafbuf = ReadBufferExtended(fc_rel, MAIN_FORKNUM, fc_leafs_to_delete[fc_i],
										 RBM_NORMAL, fc_info->strategy);
			LockBuffer(fc_leafbuf, GIST_EXCLUSIVE);
			gistcheckpage(fc_rel, fc_leafbuf);

			LockBuffer(fc_buffer, GIST_EXCLUSIVE);
			if (fc_gistdeletepage(fc_info, fc_vstate->stats,
							   fc_buffer, fc_todelete[fc_i] - fc_deleted,
							   fc_leafbuf))
				fc_deleted++;
			LockBuffer(fc_buffer, GIST_UNLOCK);

			UnlockReleaseBuffer(fc_leafbuf);
		}

		ReleaseBuffer(fc_buffer);

		/*
		 * 一旦我们查看了下行链接，就可以停止扫描，即使我们无法移除它们全部。
		 */
		fc_empty_pages_remaining -= fc_ntodelete;
	}
}

/*
 * gistdeletepage接收一个叶子页及其父页，并尝试删除该叶子页。两个页面必须被锁定。
 *
 * 即使我们第一次看到该页时是空的，可能也有并发插入者之后向其添加了一个元组。同样，下行链接可能已经移动。我们重新检查所有条件，以确保该页仍然可以删除，然后才进行任何修改。
 *
 * 如果页被删除则返回 true，如果由于并发更新而无法删除则返回 false。
 */
static bool fc_gistdeletepage(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
			   Buffer fc_parentBuffer, OffsetNumber fc_downlink,
			   Buffer fc_leafBuffer)
{
	Page		fc_parentPage = BufferGetPage(fc_parentBuffer);
	Page		fc_leafPage = BufferGetPage(fc_leafBuffer);
	ItemId		fc_iid;
	IndexTuple	fc_idxtuple;
	XLogRecPtr	fc_recptr;
	FullTransactionId fc_txid;

	/*
	 * 检查叶子页是否仍然为空且可删除。
	 */
	if (!GistPageIsLeaf(fc_leafPage))
	{
		/* 叶子页不应该变成非叶子页 */
		Assert(false);
		return false;
	}

	if (GistFollowRight(fc_leafPage))
		return false;			/* 不要对并发页面拆分进行干预 */

	if (PageGetMaxOffsetNumber(fc_leafPage) != InvalidOffsetNumber)
		return false;			/* 不再为空 */

	/*
	 * 好的，叶子页可以删除。父页中的下行链接仍然有效吗？它可能已被并发插入移动。我们可以尝试通过再次扫描页面重新找到它，如果页面被拆分可能向右移动。但现在，让我们保持简单，放弃吧。下一个 VACUUM 会处理它。
	 */
	if (PageIsNew(fc_parentPage) || GistPageIsDeleted(fc_parentPage) ||
		GistPageIsLeaf(fc_parentPage))
	{
		/* 这不应该发生，内部页面永远不会被删除 */
		Assert(false);
		return false;
	}

	if (PageGetMaxOffsetNumber(fc_parentPage) < fc_downlink
		|| PageGetMaxOffsetNumber(fc_parentPage) <= FirstOffsetNumber)
		return false;

	fc_iid = PageGetItemId(fc_parentPage, fc_downlink);
	fc_idxtuple = (IndexTuple) PageGetItem(fc_parentPage, fc_iid);
	if (BufferGetBlockNumber(fc_leafBuffer) !=
		ItemPointerGetBlockNumber(&(fc_idxtuple->t_tid)))
		return false;

	/*
	 * 一切正常，继续删除。
	 *
	 * 该页不能立即回收，因为看到下行链接的进行中的扫描可能仍会访问它。用当前的 next-XID 计数器标记该页，以便我们知道何时可以回收。一旦该 XID 变得比 GlobalXmin 年龄更大，我们就知道所有当前进行中的扫描必须已经结束。（这比需要的保守得多，但我们保持安全和简单。）
	 */
	fc_txid = ReadNextFullTransactionId();

	START_CRIT_SECTION();

	/* 将页面标记为已删除 */
	MarkBufferDirty(fc_leafBuffer);
	GistPageSetDeleted(fc_leafPage, fc_txid);
	fc_stats->pages_newly_deleted++;
	fc_stats->pages_deleted++;

	/* 从父页中移除下行链接 */
	MarkBufferDirty(fc_parentBuffer);
	PageIndexTupleDelete(fc_parentPage, fc_downlink);

	if (RelationNeedsWAL(fc_info->index))
		fc_recptr = gistXLogPageDelete(fc_leafBuffer, fc_txid, fc_parentBuffer, fc_downlink);
	else
		fc_recptr = gistGetFakeLSN(fc_info->index);
	PageSetLSN(fc_parentPage, fc_recptr);
	PageSetLSN(fc_leafPage, fc_recptr);

	END_CRIT_SECTION();

	return true;
}
